package xin.nick.system.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.util.CollectionUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.HandlerExceptionResolver;
import xin.nick.common.core.constant.SystemConstants;
import xin.nick.common.core.entity.Result;
import xin.nick.common.core.util.ResultUtil;
import xin.nick.system.domain.vo.LoginUserVO;
import xin.nick.system.entity.SystemUser;
import xin.nick.system.filter.JsonLoginFilter;
import xin.nick.system.filter.TokenAuthenticationTokenFilter;
import xin.nick.system.filter.UserIdFilter;
import xin.nick.system.manager.DynamicAuthorizationManager;
import xin.nick.system.manager.LoginManager;
import xin.nick.system.manager.SystemAuthorityManager;
import xin.nick.system.manager.SystemUserManager;
import xin.nick.system.util.SecurityUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * @author Nick
 * @since 2022/7/20/020
 */

// 自动配置 默认开启了,这里可以不配置 org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration 里面
// @EnableWebSecurity


// 关于 5.7 之后的版本,是怎么对注解式权限管理处理的
// 可以看看文章 https://blog.csdn.net/python15397/article/details/129268249
// 从 @EnableMethodSecurity 开始进入看下流程
// 主要是对注解 PreAuthorize 进行AOP 切面处理,获取用户权限列表和方法的EL表达式,循环判断判断是否有权限
// AuthorizationManagerBeforeMethodInterceptor 由这个切面类来处理

@Configuration
@Slf4j
@EnableMethodSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final ApplicationSecurityProperties applicationSecurityProperties;
    private final AuthenticationConfiguration authenticationConfiguration;
    private final TokenAuthenticationTokenFilter tokenAuthenticationTokenFilter;
    private final LoginManager loginManager;
    private final SystemUserManager systemUserManager;
    private final SystemAuthorityManager systemAuthorityManager;
    private final HandlerExceptionResolver handlerExceptionResolver;
    private final UserIdFilter userIdFilter;
    private final DynamicAuthorizationManager dynamicAuthorizationManager;


    /**
     * 放行不进过滤器的地址信息
     * 没用到,在Security 5.7 之后不推荐使用这种方式放行白名单
     * @return WebSecurityCustomizer
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {

        // 开放其他页面权限
        List<String> ignoreList = applicationSecurityProperties.getIgnoreList();
        if (CollUtil.isEmpty(ignoreList)) {
            ignoreList = new ArrayList<>();
        }

        String[] ignoreListArray = ArrayUtil.toArray(ignoreList, String.class);


        return web -> {
            // 放行忽略的地址
            web.ignoring().mvcMatchers(ignoreListArray);
            // 放行 OPTIONS 请求
            web.ignoring().mvcMatchers(HttpMethod.OPTIONS);
        };
    }

    /**
     * 配置过滤器
     *
     * @param http HttpSecurity
     * @return SecurityFilterChain
     * @throws Exception Exception
     */
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        // 禁用 csrf
        http.csrf().disable();

        // 设置跨域
        http.cors().configurationSource(corsConfigurationSource());

        // 去掉session,这是使用token+Redis的方式进行的授权认证,
        // 如果不用Token+Redis的话,下面可以注释掉,使用默认配置
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 异常处理配置
        ExceptionHandlingConfigurer<HttpSecurity> exceptionHandlingConfigurer = http.exceptionHandling();

        // 配置认证失败处理 异常返回
        exceptionHandlingConfigurer.authenticationEntryPoint((request, response, authException) ->
                // 交给全局异常处理
                handlerExceptionResolver.resolveException(request, response, null, authException)
        );

        // 配置没有权限处理 异常返回
        exceptionHandlingConfigurer.accessDeniedHandler((request, response, authException) ->
                // 交给全局异常处理
                handlerExceptionResolver.resolveException(request, response, null, authException)
        );

        // 处理退出登录逻辑
        http.logout().addLogoutHandler((request, response, authentication) -> {
            UserDetails currentUser = SecurityUtil.getCurrentUserNoCheck();
            if (Objects.nonNull(currentUser)) {
                String username = currentUser.getUsername();
                SystemUser systemUser = systemUserManager.getUserByUsername(username);
                if (Objects.nonNull(systemUser)) {
                    Long userId = systemUser.getUserId();
                    Boolean removeState = loginManager.removeLoginUserToken(systemUser.getUserId());
                    log.info("用户退出登录: currentUserId: {}, 退出情况: {}", userId, removeState);
                }

            }

        }).logoutSuccessHandler((request, response, authentication) ->
                ResultUtil.responseResult(response, Result.success("退出登录成功"))
        );

        // 处理登录逻辑
        http.formLogin()
                .failureHandler((request, response, exception) -> {
                    log.warn("登录出现异常: {}", exception.getMessage());
                    // 交给全局异常处理
                    handlerExceptionResolver.resolveException(request, response, null, exception);

                }).successHandler((request, response, authentication) -> {

                    UserDetails principal = (UserDetails) authentication.getPrincipal();

                    String username = principal.getUsername();
                    SystemUser systemUser = systemUserManager.getUserByUsername(username);
                    LoginUserVO loginUserVO = loginManager.buildLoginUserVO(systemUser);

                    ResultUtil.responseResult(response, Result.success(loginUserVO));
                });


        // 指定拦截需要授权才可以访问的路径
        List<String> authList = applicationSecurityProperties.getAuthList();
        if (!CollectionUtils.isEmpty(authList)) {
            for (String authPath : authList) {
//                http.authorizeHttpRequests().mvcMatchers(authPath).authenticated();
                http.authorizeHttpRequests().mvcMatchers(authPath).access(dynamicAuthorizationManager);
            }
        }

        // 开放白名单页面权限
        List<String> whiteList = applicationSecurityProperties.getWhiteList();
        if (!CollectionUtils.isEmpty(whiteList)) {
            for (String ignoreUrl : whiteList) {
                http.authorizeHttpRequests().mvcMatchers(ignoreUrl).permitAll();
            }
        }

        // 验证,登录地址放行
        http.authorizeHttpRequests().mvcMatchers(SystemConstants.CODE_PATH).permitAll();
        http.authorizeHttpRequests().mvcMatchers(SystemConstants.LOGIN_PATH).permitAll();
        http.authorizeHttpRequests().mvcMatchers(SystemConstants.ERROR_PATH).permitAll();

        // 指定所有的OPTIONS请求直接通过
        http.authorizeHttpRequests().mvcMatchers(HttpMethod.OPTIONS).permitAll();

        // 默认其他所有的的都需要权限校验,通过数据库菜单方式鉴权
//        http.authorizeRequests().anyRequest().authenticated();
//        http.authorizeHttpRequests().anyRequest().authenticated();
        http.authorizeHttpRequests().anyRequest().access(dynamicAuthorizationManager);


        // token过滤器退出登录前需要校验
        http.addFilterBefore(tokenAuthenticationTokenFilter, LogoutFilter.class);

        // token 拦截校验在 账号密码校验之前,设置了验证信息则不需要校验账号密码了
        http.addFilterBefore(tokenAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // Json方式登录放在 UsernamePasswordAuthenticationFilter 之前
        http.addFilterBefore(getJsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);

        // userId的设置,放在用户账号密码校验之后
        http.addFilterAfter(userIdFilter, UsernamePasswordAuthenticationFilter.class);


        return http.build();
    }

    /**
     * 配置跨域
     */
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        // 允许跨域站点
        configuration.setAllowedOrigins(Collections.singletonList("*"));
        // 允许跨域的方法
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH",
                "DELETE", "OPTIONS"));
        // 允许跨域的请求头
        configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type",
                "x-auth-token"));
        // 允许暴露的请求头
        configuration.setExposedHeaders(Collections.singletonList("x-auth-token"));
        // 允许携带凭据
        configuration.setAllowCredentials(true);

        // 配置有效期
        configuration.setMaxAge(3600L);

        //
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 对所有URL生效
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

    /**
     * 编写AuthenticationManager的bean
     */
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }


    /**
     * 设置密码加密类
     *
     * @return BCryptPasswordEncoder
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * Json参数方式登录
     *
     * @return
     * @throws Exception
     */
    @Bean
    public JsonLoginFilter getJsonLoginFilter() throws Exception {
        JsonLoginFilter filter = new JsonLoginFilter();

        // 设置登录成功回调
        filter.setAuthenticationSuccessHandler((request, response, authentication) -> {

            UserDetails principal = (UserDetails) authentication.getPrincipal();

            String username = principal.getUsername();
            SystemUser systemUser = systemUserManager.getUserByUsername(username);
            LoginUserVO loginUserVO = loginManager.buildLoginUserVO(systemUser);

            ResultUtil.responseResult(response, Result.success(loginUserVO));
        });

        // 设置登录失败回调
        filter.setAuthenticationFailureHandler((request, response, exception) -> {
            log.warn("登录失败: {}", exception.getMessage());
            // 交给全局异常处理
            handlerExceptionResolver.resolveException(request, response, null, exception);

        });

        // 设置认证管理器
        filter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());

        // 设置监听的地址
        filter.setFilterProcessesUrl(SystemConstants.LOGIN_PATH);

        // SpringBoot3,且没有使用Redis和JWT,仅使用Session的时候 需要设置一下
//        filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());

        return filter;
    }

}
